﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MugenInjection;
using MugenInjection.Core.Components;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Components;
using MugenInjection.Parameters;
using Should;
#if NET2
using Func = MugenInjection.Delegates.Func<object, object>;
#else
using Func = System.Func<object, object>;
#endif

namespace MugenInjectionTest.Components
{

    public class ArrayParametersClass
    {
        public IList<string> St;

        public ArrayParametersClass(IEnumerable<string> test, IDisposable[] disposable)
        {
        }

        public ICollection<IDisposable> Prop { get; set; }
    }

    public class SimpleParameterClass
    {
        public string Test { get; set; }
        public IDisposable Disposable { get; set; }

        public string St;

        public SimpleParameterClass(string test, IDisposable disposable)
        {
            Test = test;
            Disposable = disposable;
        }

        public IDisposable Prop { get; set; }
    }

    public delegate ConverterResult ConverterDelegate(IBindingContext bindingContext, out Type originalType, out Func convertAction);

    [TestClass]
    public class BindingActivatorComponentTest : ComponentTestBase<IBindingActivatorComponent>
    {
        [TestMethod]
        public void InitActivatorTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.ActivatorToInject.ShouldNotBeNull();
                bindingActivatorComponent.Converters.ShouldNotBeEmpty();
            }
        }

        [TestMethod]
        public void ActivateBindingTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;

                IBindingContext bindingContext = injector.CreateContext(typeof(IDisposable), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());
                IBinding binding = injector.BindWithManualBuild<IDisposable>().ToConstant(injector).Build();
                object activate = bindingActivatorComponent.Activate(binding, bindingContext);
                activate.ShouldEqual(injector);
            }
        }

        [TestMethod]
        public void GetSimpleParametersTest()
        {
            ConstructorInfo constructorInfo = typeof(SimpleParameterClass).GetConstructors()[0];
            const string value = "value";
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(SimpleParameterClass));
                injector.Bind<string>().ToConstant(value + Guid.NewGuid());
                injector.Bind<IDisposable>().ToConstant(injector);
                IBindingContext bindingContext = injector.CreateContext(typeof(SimpleParameterClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(2);
                parameters[0].ShouldEqual(value);
                parameters[1].ShouldEqual(injector);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();

                const string newValue = "newValue";
                bindingContext.Update(bindingContext.Service, bindingContext.Member, bindingContext.ParameterInfo,
                                      bindingContext.Injector, bindingContext.CallInjector,
                                      new[] { new ConstructorParameter("test", newValue) },
                                      new Dictionary<string, object>());
                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(2);
                parameters[0].ShouldEqual(newValue);
                parameters[1].ShouldEqual(injector);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();
            }
        }

        [TestMethod]
        public void GetSimpleParameterTest()
        {
            PropertyInfo property = typeof(SimpleParameterClass).GetProperty("Prop");
            const string value = "value";
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(SimpleParameterClass));
                injector.Bind<string>().ToConstant(value + Guid.NewGuid());
                injector.Bind<IDisposable>().ToConstant(injector);
                IBindingContext bindingContext = injector.CreateContext(typeof(SimpleParameterClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object propValue = bindingActivatorComponent.GetParameter(property, bindingContext);
                propValue.ShouldEqual(injector);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();

                bindingContext.Update(bindingContext.Service, bindingContext.Member, bindingContext.ParameterInfo,
                                      bindingContext.Injector, bindingContext.CallInjector,
                                      new IInjectionParameter[] { new PropertyParameter("Prop", value: null) },
                                      new Dictionary<string, object>());
                propValue = bindingActivatorComponent.GetParameter(property, bindingContext);
                propValue.ShouldBeNull();
                
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();
            }
        }

        [TestMethod]
        public void GetArrayParametersTest()
        {
            ConstructorInfo constructorInfo = typeof(ArrayParametersClass).GetConstructors()[0];
            const string value = "value";
            const string value2 = "value";
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(ArrayParametersClass));
                injector.Bind<string>().ToConstant(value2).WhenInto(typeof(ArrayParametersClass));
                injector.Bind<string>().ToConstant(value + Guid.NewGuid()).WhenInto(typeof(SimpleParameterClass));
                injector.Bind<IDisposable>().ToConstant(injector);
                injector.Bind<IDisposable>().ToConstant(injector);

                IBindingContext bindingContext = injector.CreateContext(typeof(ArrayParametersClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                object[] parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                parameters.Length.ShouldEqual(2);
                var enumerable = (IEnumerable<string>)parameters[0];
                enumerable.Count().ShouldEqual(2);
                enumerable.ElementAt(0).ShouldEqual(value);
                enumerable.ElementAt(1).ShouldEqual(value2);

                var disposables = (IDisposable[])parameters[1];
                disposables.Length.ShouldEqual(2);
                disposables[0].ShouldEqual(injector);
                disposables[1].ShouldEqual(injector);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();

                const string newValue = "newValue";
                var newParam = new[] { newValue };
                var dispParam = new IDisposable[] { injector };
                bindingContext.Update(bindingContext.Service, bindingContext.Member, bindingContext.ParameterInfo,
                                      bindingContext.Injector, bindingContext.CallInjector,
                                      new[]
                                          {
                                              new ConstructorParameter("test", newParam),
                                              new ConstructorParameter("disposable", dispParam)
                                          },
                                      new Dictionary<string, object>());

                parameters = bindingActivatorComponent.GetParameters(constructorInfo, bindingContext);
                newParam.ShouldEqual(parameters[0]);
                dispParam.ShouldEqual(parameters[1]);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();
            }
        }

        [TestMethod]
        public void GetCollectionParameterTest()
        {
            PropertyInfo property = typeof(ArrayParametersClass).GetProperty("Prop");
            const string value = "value";
            const string value2 = "value";
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;

                injector.Bind<string>().ToConstant(value).WhenInto(typeof(ArrayParametersClass));
                injector.Bind<string>().ToConstant(value2).WhenInto(typeof(ArrayParametersClass));
                injector.Bind<string>().ToConstant(value + Guid.NewGuid()).WhenInto(typeof(SimpleParameterClass));
                injector.Bind<IDisposable>().ToConstant(injector);
                injector.Bind<IDisposable>().ToConstant(injector);

                IBindingContext bindingContext = injector.CreateContext(typeof(ArrayParametersClass), null, null,
                                                                        new IInjectionParameter[0],
                                                                        new Dictionary<string, object>());

                var propValue =
                    (ICollection<IDisposable>)bindingActivatorComponent.GetParameter(property, bindingContext);
                
                propValue.Count.ShouldEqual(2);
                propValue.ElementAt(0).ShouldEqual(injector);
                propValue.ElementAt(1).ShouldEqual(injector);

                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();

                const string newValue = "newValue";
                var newParam = new[] { newValue };
                var dispParam = new IDisposable[] { injector };
                bindingContext.Update(bindingContext.Service, bindingContext.Member, bindingContext.ParameterInfo,
                                      bindingContext.Injector, bindingContext.CallInjector,
                                      new IInjectionParameter[] { new PropertyParameter("Prop", dispParam) },
                                      new Dictionary<string, object>());
                propValue = (ICollection<IDisposable>)bindingActivatorComponent.GetParameter(property, bindingContext);
                propValue.ShouldEqual(dispParam);
                bindingContext.Member.ShouldBeNull();
                bindingContext.ParameterInfo.ShouldBeNull();
            }
        }

        [TestMethod]
        public void ShouldCacheConverterResultIfWithCacheTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                int countTry = 0;
                int countGet = 0;
                var cacheConverter = new FakeConverter { ServiceType = MugenInjection.Infrastructure.ServiceType.Simple };
                cacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                    out Func action) =>
                                                       {
                                                           countTry++;
                                                           type = context.Service;
                                                           action = o =>
                                                                        {
                                                                            countGet++;
                                                                            o.ShouldEqual(value);
                                                                            return o;
                                                                        };
                                                           return ConverterResult.SupportedWithCache;
                                                       };
                bindingActivatorComponent.AddConverter(cacheConverter);

                var s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(1);
                countTry.ShouldEqual(1);

                s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(2);
                countTry.ShouldEqual(1);
            }
        }

        [TestMethod]
        public void ShouldNotCacheConverterResultIfWithoutCacheTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                int countTry = 0;
                int countGet = 0;
                var cacheConverter = new FakeConverter { ServiceType = MugenInjection.Infrastructure.ServiceType.Simple };
                cacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                    out Func action) =>
                {
                    countTry++;
                    type = context.Service;
                    action = o =>
                    {
                        countGet++;
                        o.ShouldEqual(value);
                        return o;
                    };
                    return ConverterResult.SupportedWithoutCache;
                };
                bindingActivatorComponent.AddConverter(cacheConverter);

                var s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(1);
                countTry.ShouldEqual(1);

                s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(2);
                countTry.ShouldEqual(2);
            }
        }

        [TestMethod]
        public void ShouldClearCacheOnConverterSupportedTypeRemovedTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                int countTry = 0;
                int countGet = 0;
                var cacheConverter = new FakeConverter { ServiceType = MugenInjection.Infrastructure.ServiceType.Simple };
                cacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                    out Func action) =>
                {
                    countTry++;
                    type = context.Service;
                    action = o =>
                    {
                        countGet++;
                        o.ShouldEqual(value);
                        return o;
                    };
                    return ConverterResult.SupportedWithCache;
                };
                bindingActivatorComponent.AddConverter(cacheConverter);

                var s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(1);
                countTry.ShouldEqual(1);

                cacheConverter.OnSupportedTypeChanged(TypeChangedAction.Remove, typeof(string));
                s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(2);
                countTry.ShouldEqual(2);
            }
        }

        [TestMethod]
        public void ShouldClearCacheOnConverterAddedTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                int countTry = 0;
                int countGet = 0;
                var cacheConverter = new FakeConverter { ServiceType = MugenInjection.Infrastructure.ServiceType.Simple };
                cacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                    out Func action) =>
                {
                    countTry++;
                    type = context.Service;
                    action = o =>
                    {
                        countGet++;
                        o.ShouldEqual(value);
                        return o;
                    };
                    return ConverterResult.SupportedWithCache;
                };
                bindingActivatorComponent.AddConverter(cacheConverter);


                var s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(1);
                countTry.ShouldEqual(1);

                bindingActivatorComponent.AddConverter(new FakeConverter() { Priority = -10 });
                s = injector.Get<string>();
                s.ShouldEqual(value);
                countGet.ShouldEqual(2);
                countTry.ShouldEqual(2);
            }
        }

        [TestMethod]
        public void ShouldUsePriorityToCallConvertersTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";
                const string lowPriority = "low";
                const string normalPriority = "normal";

                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                var lowCacheConverter = new FakeConverter
                                            {
                                                ServiceType =
                                                    MugenInjection.Infrastructure.ServiceType.Simple,
                                                Priority = int.MinValue
                                            };
                lowCacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                    out Func action) =>
                {
                    type = context.Service;
                    action = o => lowPriority;
                    return ConverterResult.SupportedWithCache;
                };
                bindingActivatorComponent.AddConverter(lowCacheConverter);

                var normalCacheConverter = new FakeConverter
                                               {
                                                   ServiceType =
                                                       MugenInjection.Infrastructure.ServiceType.Simple,
                                                   Priority = int.MaxValue
                                               };
                normalCacheConverter.ConverterDelegate = (IBindingContext context, out Type type,
                                                          out Func action) =>
                                                             {
                                                                 type = context.Service;
                                                                 action = o => normalPriority;
                                                                 return ConverterResult.SupportedWithCache;
                                                             };
                bindingActivatorComponent.AddConverter(normalCacheConverter);


                var s = injector.Get<string>();
                s.ShouldEqual(normalPriority);
            }
        }

        [TestMethod]
        public void ShouldWorkWithoutConvertersTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                const string value = "value";

                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                injector.Bind<string>().ToConstant(value);

                injector.Get<string>().ShouldEqual(value);
            }
        }

        [TestMethod]
        public void ShouldCopyConvertersOnCloneTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                IBindingActivatorComponent bindingActivatorComponent = CreateNew();
                injector.Components.BindingActivatorComponent = bindingActivatorComponent;
                bindingActivatorComponent.RemoveAllConverters();
                var cacheConverter = new FakeConverter { ServiceType = MugenInjection.Infrastructure.ServiceType.Simple };
                bindingActivatorComponent.AddConverter(cacheConverter);

                var component = (IBindingActivatorComponent)bindingActivatorComponent.Clone();
                component.Converters.Count.ShouldEqual(1);
                component.Converters.Contains(cacheConverter).ShouldBeTrue();
            }
        }

        #region Nested type: FakeConverter

        public class FakeConverter : IConverter
        {
            #region Implementation of IConverter

            /// <summary>
            /// Initializes a new instance of the <see cref="T:System.Object"/> class.
            /// </summary>
            public FakeConverter()
            {
                ServiceType = ServiceType.Simple;
            }

            public ConverterDelegate ConverterDelegate { get; set; }

            /// <summary>
            ///     Gets the service type of converter.
            /// </summary>
            public ServiceType ServiceType { get; set; }

            /// <summary>
            ///     Gets the priority.
            /// </summary>
            public int Priority { get; set; }

            /// <summary>
            ///     Gets the supported types.
            /// </summary>
            public IEnumerable<Type> SupportedTypes { get; set; }

            /// <summary>
            ///     Tries to convert instance.
            /// </summary>
            /// <param name="bindingContext">The specified type to convert from.</param>
            /// <param name="originalType">The type to converted to.</param>
            /// <param name="convertAction">An delegate to convert instance.</param>
            /// <returns>
            ///     If <c>true</c> can be converted; otherwise <c>false</c>.
            /// </returns>
            public ConverterResult TryConvert(IBindingContext bindingContext, out Type originalType, out Func convertAction)
            {
                return ConverterDelegate(bindingContext, out originalType, out convertAction);
            }

            /// <summary>
            /// Clears cache, if any.
            /// </summary>
            public void ClearCache()
            {
            }

            public event TypeChangedDelegate SupportedTypeChanged;

            public void OnSupportedTypeChanged(TypeChangedAction action, Type type)
            {
                var handler = SupportedTypeChanged;
                if (handler != null) handler(this, action, type);
            }

            #endregion
        }

        #endregion

        #region Overrides of ComponentTestBase<IBindingActivatorComponent>

        protected override IBindingActivatorComponent CreateNew()
        {
            return new BindingActivatorComponent();
        }

        #endregion
    }
}